﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Syk.Library.Common;

namespace Syk.Library.Scheduling
{
    public sealed class Scheduler<TSchedulerItem> : IDisposable
     where TSchedulerItem : ISchedulerItem
    {
        private Timer m_Timer;
        private bool m_Disposed;
        private object m_SyncRoot = new object();
        private readonly SortedList<DateTime, List<TSchedulerItem>> m_SchedulerItems;
        private readonly ISchedulingConfiguration m_SchedulingConfiguration;
        private readonly IDateTimePreciseService m_DateTimePreciseService;
        public event EventHandler<EventArgs<TSchedulerItem>> SchedulerItemExecution = delegate { };

        #region ctor's

        public Scheduler(ISchedulingConfiguration schedulingConfiguration, IDateTimePreciseService dateTimePreciseService)
        {
            m_SchedulingConfiguration = schedulingConfiguration;
            m_DateTimePreciseService = dateTimePreciseService;
            m_Timer = new Timer(new TimerCallback(OnTimerElapsed));
            m_SchedulerItems = new SortedList<DateTime, List<TSchedulerItem>>();
        }

        #endregion



        public DateTime CurrentTime
        {
            get
            {
                return m_DateTimePreciseService.GetTime().Normalize();
            }
        }

        private void OnTimerElapsed(object state)
        {
            lock (m_SyncRoot)
            {
                while (m_SchedulerItems.Count > 0 && m_SchedulerItems.Keys[0] <= CurrentTime)
                {
                    List<TSchedulerItem> schedulerItems = m_SchedulerItems.Values[0];
                    m_SchedulerItems.RemoveAt(0);
                    foreach (TSchedulerItem item in schedulerItems)
                    {
                        OnSchedulerItemExecution(item);
                    }
                }
                MeasureTimer();
            }
        }

        private void MeasureTimer()
        {
            if (m_Disposed)
            {
                return;
            }
            int dueTime = Timeout.Infinite;
            if (m_SchedulerItems.Count > 0)
            {
                TimeSpan offset = m_SchedulerItems.Keys[0] - CurrentTime;
                //In case of late(delay) scheduling
                dueTime = Convert.ToInt32(Math.Max(0, offset.TotalMilliseconds));
            }
            m_Timer.Change(dueTime, Timeout.Infinite);
        }

        private void OnSchedulerItemExecution(TSchedulerItem schedulerItem)
        {
            var invocationList = SchedulerItemExecution.GetInvocationList()
                .Select(handler => (EventHandler<EventArgs<TSchedulerItem>>)handler);

            foreach (var handler in invocationList)
            {
                handler.BeginInvoke(this, new EventArgs<TSchedulerItem>(schedulerItem), null, null);
            }
        }

        public void Clear()
        {
            ThrowIfDisposed();
            lock (m_SyncRoot)
            {
                m_Timer.Change(Timeout.Infinite, Timeout.Infinite);
                m_SchedulerItems.Clear();
            }
        }

        public IEnumerable<TSchedulerItem> SchedulerItems
        {
            get
            {
                ThrowIfDisposed();
                lock (m_SyncRoot)
                {
                    return (from schedulerItems in m_SchedulerItems.Values
                            from schedulerItem in schedulerItems
                            select schedulerItem).ToArray();
                }
            }
        }

        public bool Delete(TSchedulerItem schedulerItem)
        {
            ThrowIfDisposed();

            bool deleted = false;
            lock (m_SyncRoot)
            {
                List<TSchedulerItem> schedulerItems;
                if (m_SchedulerItems.TryGetValue(schedulerItem.StartTime, out schedulerItems))
                {
                    if (schedulerItems.Count > 0)
                    {
                        deleted = schedulerItems.Remove(schedulerItem);
                        if (schedulerItems.Count == 0)
                        {
                            deleted = m_SchedulerItems.Remove(schedulerItem.StartTime);
                        }
                        MeasureTimer();
                    }
                }
            }
            return deleted;
        }
        public void Add(TSchedulerItem schedulerItem)
        {
            ThrowIfDisposed();
            ThrowIfExpired(schedulerItem.StartTime);
            lock (m_SyncRoot)
            {
                List<TSchedulerItem> schedulerItems;
                if (!m_SchedulerItems.TryGetValue(schedulerItem.StartTime, out schedulerItems))
                {
                    schedulerItems = new List<TSchedulerItem>();
                    m_SchedulerItems.Add(schedulerItem.StartTime, schedulerItems);
                }
                schedulerItems.Add(schedulerItem);

                MeasureTimer();
            }
        }

        public void AddRange(IEnumerable<TSchedulerItem> schedulerItems)
        {
            ThrowIfDisposed();

            lock (m_SyncRoot)
            {
                foreach (TSchedulerItem item in schedulerItems)
                {
                    Add(item);

                }
            }
        }

        private void ThrowIfDisposed()
        {
            if (m_Disposed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }

        private void ThrowIfExpired(DateTime dateTime)
        {
            if (dateTime.Add(m_SchedulingConfiguration.GraceTimeForExpireSchedulerItem) <= CurrentTime)
            {
                throw new InvalidOperationException("expired scheduler item");
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (!m_Disposed)
            {
                lock (m_SyncRoot)
                {
                    if (!m_Disposed)
                    {
                        m_Timer.Dispose();
                        m_Disposed = true;
                    }
                }
            }
        }

        #endregion

    }
}
